// LruHashtable - a Hashtable that expires least-recently-used objects
//
// Copyright (C) 1996 by Jef Poskanzer <jef@mail.acme.com>.  All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// Visit the ACME Labs Java page for up-to-date versions of this and other
// fine Java utilities: http://www.acme.com/java/

package Acme;

import java.util.Enumeration;
import java.util.Hashtable;

/// A Hashtable that expires least-recently-used objects.
// <P>
// Use just like java.util.Hashtable, except that the initial-capacity
// parameter is required.  Instead of growing bigger than that size,
// it will throw out objects that haven't been looked at in a while.
// <P>
// <A HREF="/resources/classes/Acme/LruHashtable.java">Fetch the software.</A><BR>
// <A HREF="/resources/classes/Acme.tar.gz">Fetch the entire Acme package.</A>
// <P>
// @see java.util.Hashtable

public class LruHashtable extends Hashtable
    {

    // Number of buckets.
    private static final int nBuckets = 2;

    // Load factor.
    private float loadFactor;

    // When count exceeds this threshold, expires the old table.
    private int threshold;

    // Capacity of each bucket.
    private int eachCapacity;

    // The tables.
    private Hashtable oldTable;
    private Hashtable newTable;

    /// Constructs a new, empty hashtable with the specified initial 
    // capacity and the specified load factor.
    // Unlike a plain Hashtable, an LruHashtable will never grow or
    // shrink from this initial capacity.
    // @param initialCapacity the initial number of buckets
    // @param loadFactor a number between 0.0 and 1.0, it defines
    //		the threshold for expiring old entries
    // @exception IllegalArgumentException If the initial capacity
    // is less than or equal to zero.
    // @exception IllegalArgumentException If the load factor is
    // less than or equal to zero.
    public LruHashtable( int initialCapacity, float loadFactor )
	{
	// We have to call a superclass constructor, but we're not actually
	// going to use it at all.  The only reason we want to extend Hashtable
	// is for type conformance.  So, make a parent hash table of minimum
	// size and then ignore it.
	super( 1 );

	if ( initialCapacity <= 0 || loadFactor <= 0.0 )
	    throw new IllegalArgumentException();
	this.loadFactor = loadFactor;
	threshold = (int) ( initialCapacity * loadFactor ) - 1;
	eachCapacity = initialCapacity / nBuckets + 1;
	oldTable = new Hashtable( eachCapacity, loadFactor );
	newTable = new Hashtable( eachCapacity, loadFactor );
	}

    /// Constructs a new, empty hashtable with the specified initial 
    // capacity.
    // Unlike a plain Hashtable, an LruHashtable will never grow or
    // shrink from this initial capacity.
    // @param initialCapacity the initial number of buckets
    public LruHashtable( int initialCapacity )
	{
	this( initialCapacity, 0.75F );
	}

    /// Returns the number of elements contained in the hashtable. 
    public int size()
	{
	return newTable.size() + oldTable.size();
	}

    /// Returns true if the hashtable contains no elements.
    public boolean isEmpty()
	{
	return size() == 0;
	}

    /// Returns an enumeration of the hashtable's keys.
    // @see LruHashtable#elements
    // @see Enumeration
    public synchronized Enumeration keys()
	{
	return new LruHashtableEnumerator( oldTable, newTable, true );
	}

    /// Returns an enumeration of the elements. Use the Enumeration methods 
    // on the returned object to fetch the elements sequentially.
    // @see LruHashtable#keys
    // @see Enumeration
    public synchronized Enumeration elements()
	{
	return new LruHashtableEnumerator( oldTable, newTable, false );
	}

    /// Returns true if the specified object is an element of the hashtable.
    // This operation is more expensive than the containsKey() method.
    // @param value the value that we are looking for
    // @exception NullPointerException If the value being searched 
    // for is equal to null.
    // @see LruHashtable#containsKey
    public synchronized boolean contains( Object value )
	{
	if ( newTable.contains( value ) )
	    return true;
	if ( oldTable.contains( value ) )
	    {
	    // We would like to move the object from the old table to the
	    // new table.  However, we need keys to re-add the objects, and
	    // there's no good way to find all the keys for the given object.
	    // We'd have to enumerate through all the keys and check each
	    // one.  Yuck.  For now we just punt.  Anyway, contains() is
	    // probably not a commonly-used operation.
	    return true;
	    }
	return false;
	}

    /// Returns true if the collection contains an element for the key.
    // @param key the key that we are looking for
    // @see LruHashtable#contains
    public synchronized boolean containsKey( Object key )
	{
	if ( newTable.containsKey( key ) )
	    return true;
	if ( oldTable.containsKey( key ) )
	    {
	    // Move object from old table to new table.
	    Object value = oldTable.get( key );
	    newTable.put( key, value );
	    oldTable.remove( key );
	    return true;
	    }
	return false;
	}

    /// Gets the object associated with the specified key in the 
    // hashtable.
    // @param key the specified key
    // @returns the element for the key or null if the key
    // 		is not defined in the hash table.
    // @see LruHashtable#put
    public synchronized Object get( Object key )
	{
	Object value;
	value = newTable.get( key );
	if ( value != null )
	    return value;
	value = oldTable.get( key );
	if ( value != null )
	    {
	    // Move object from old table to new table.
	    newTable.put( key, value );
	    oldTable.remove( key );
	    return value;
	    }
	return null;
	}

    /// Puts the specified element into the hashtable, using the specified
    // key.  The element may be retrieved by doing a get() with the same key.
    // The key and the element cannot be null. 
    // @param key the specified key in the hashtable
    // @param value the specified element
    // @exception NullPointerException If the value of the element 
    // is equal to null.
    // @see LruHashtable#get
    // @return the old value of the key, or null if it did not have one.
    public synchronized Object put( Object key, Object value )
	{
	Object oldValue = newTable.put( key, value );
	if ( oldValue != null )
	    return oldValue;
	oldValue = oldTable.get( key );
	if ( oldValue != null )
	    oldTable.remove( key );
	else
	    {
	    if ( size() >= threshold )
		{
		// Rotate the tables.
		oldTable = newTable;
		newTable = new Hashtable( eachCapacity, loadFactor );
		} 
	    }
	return oldValue;
	}

    /// Removes the element corresponding to the key. Does nothing if the
    // key is not present.
    // @param key the key that needs to be removed
    // @return the value of key, or null if the key was not found.
    public synchronized Object remove( Object key )
	{
	Object oldValue = newTable.remove( key );
	if ( oldValue == null )
	    oldValue = oldTable.remove( key );
	return oldValue;
	}

    /// Clears the hash table so that it has no more elements in it.
    public synchronized void clear()
	{
	newTable.clear();
	oldTable.clear();
	}

    /// Creates a clone of the hashtable. A shallow copy is made,
    // the keys and elements themselves are NOT cloned. This is a
    // relatively expensive operation.
    public synchronized Object clone()
	{
	LruHashtable n = (LruHashtable) super.clone();
	n.newTable = (Hashtable) n.newTable.clone();
	n.oldTable = (Hashtable) n.oldTable.clone();
	return n;
	}

    // toString() can be inherited.

    }


class LruHashtableEnumerator implements Enumeration
    {
    Enumeration oldEnum;
    Enumeration newEnum;
    boolean old;

    LruHashtableEnumerator( Hashtable oldTable, Hashtable newTable, boolean keys )
	{
	if ( keys )
	    {
	    oldEnum = oldTable.keys();
	    newEnum = newTable.keys();
	    }
	else
	    {
	    oldEnum = oldTable.elements();
	    newEnum = newTable.elements();
	    }
	old = true;
	}
	
    public boolean hasMoreElements()
	{
	boolean r;
	if ( old )
	    {
	    r = oldEnum.hasMoreElements();
	    if ( ! r )
		{
		old = false;
		r = newEnum.hasMoreElements();
		}
	    }
	else
	    r = newEnum.hasMoreElements();
	return r;
	}

    public Object nextElement()
	{
	if ( old )
	    return oldEnum.nextElement();
	return newEnum.nextElement();
	}
		    
    }
